import {
  isIDC,
  isIDS,
  isVChar,
} from './prop'

import {
  Module,
  Note,
  Identifier,
} from '../ast/types'

import sign, {
  keyOf,
  isKeyword,
  indexOf,
} from '../ast/sign'

import beFn     from './fn'
import beType   from './type'
import beConst  from './const'
import beEnum   from './enum'
import beTrait  from './trait'
import beUse    from './use'

const NLS = [
  0x0A,   // LF
  0x0B,   // VT
  0x0C,   // FF
  0x0D,   // CR
  0x85,   // NEL
  0x2028, // LS
  0x2029, // PS
]

const
  MIN = Number.MIN_SAFE_INTEGER,

  IllIndent = 'illegal indentation',
  IDENT     = sign.ident,
  DOLLAR    = sign['$'],
  MINUS     = sign['-'],
  PLUSx     = sign['+x'],
  MINUSx    = sign['-x'],

  IGNORE    = sign['_'],
  QMARK     = sign['?'],
  AT        = sign['@'],
  COLON     = sign[':'],
  COMMA     = sign[','],
  DOT       = sign['.'],
  LPARENS   = sign['('],
  RPARENS   = sign[')'],
  LBRACKETS = sign['['],
  RBRACKETS = sign[']'],
  LBRACES   = sign['{'],
  RBRACES   = sign['}'],

  IDChar    = Array.from('`~!@#$%^&*()_-+={}[]|\\:;\'<>,.?/')
    .map(s => s.charCodeAt(0)).sort((a, b) => a > b && 1 || -1),

  TOKEN = keysSort(sign).reduce((trie, key) => {
    let val = sign[key]
    if (val && val !== IDENT && val !== PLUSx && val !== MINUSx)
      mergeSign(trie, key, val)
    return trie
  }, [0])

const
  DECL = [
    ['pub fn',    s => s.rune === 0x20 && beFn   ],
    ['pub type',  s => s.rune === 0x20 && beType ],
    ['pub enum',  s => s.rune === 0x20 && beEnum ],
    ['pub trait', s => s.rune === 0x20 && beTrait],
    ['extern fn', s => s.rune === 0x20 && beFn   ],
    ['fn',        s => s.rune === 0x20 && beFn   ],
    ['type',      s => s.rune === 0x20 && beType ],
    ['enum',      s => s.rune === 0x20 && beEnum ],
    ['trait',     s => s.rune === 0x20 && beTrait],
    ['use',       s => !s.isIDC() && beUse  ],
    ['pub const', s => !s.isIDC() && beConst],
    ['const',     s => !s.isIDC() && beConst],
  ].reduce((trie, a) => {
    mergeAction(trie, a[0], a[1])
    return trie
  }, [null])

function keysSort (sign) {
  return Object.freeze(Object.keys(sign).sort((a, b) =>
    indexOf(sign[a]) < indexOf(sign[b]) && -1 || 1
  ))
}

function mergeAction (target, word, action) {
  let trie = target
  Array.from(word).forEach(s => {
    let
      cp = s.charCodeAt(0),
      id = trie.lastIndexOf(cp) + 1
    if (id)
      trie = trie[id]
    else {
      let tree = [null]
      trie.push(cp, tree)
      trie = tree
    }
  })
  if (trie[0]) throw Error(`${word} already exists`)

  trie[0] = action
  return target
}

function mergeSign (target, word, sign) {
  let trie = target
  Array.from(word).forEach(s => {
    let
      cp = s.charCodeAt(0),
      id = trie.lastIndexOf(cp) + 1
    if (id)
      trie = trie[id]
    else {
      let tree = [0]
      trie.push(cp, tree)
      trie = tree
    }
  })

  if (trie[0]) throw Error(`${word} already exists`)

  trie[0] = sign
  return target
}

function match (s, trie) {
  while (trie) {
    let cp = s.rune, i = trie.length - 2
    while (i > 0) {
      if (cp === trie[i]) break
      i -= 2
    }
    if (i < 1) break
    trie = trie[i + 1]
    s.step()
  }

  return trie[0]
}

function Id (s) {
  return new Identifier(s.text())
    .start(s.start).end(s.end)
}

function beDollarIdent (s) {
  if (!s.isAlpha() && !IDChar.includes(s.rune))
    s.unexpected()
  s.step()
  while (s.isAlpha() ||  IDChar.includes(s.rune))
    s.step()
  return IDENT
}

function toHex (cp) {
  let s = cp.toString(16).toUpperCase()
  return '0'.repeat(s.length & 1) + s
}

function isAlpha (cp) {
  return cp >= 0x61 && cp <= 0x7A
      || cp >= 0x41 && cp <= 0x5A
}

function toString (x) {
  return x && x.constructor.name || 'null'
}

function IDC (s) {
  if (s.rune == 0x2D) {
    s.step()
    if (!isIDC(s.rune)) s.unexpected()
  }
  while (isIDC(s.rune)) {
    s.step()
    if (s.rune == 0x2D) {
      s.step()
      if (!isIDC(s.rune)) s.unexpected()
    }
  }
  return IDENT
}

export default class Parser {
  constructor (module_path) {

    this.result = new Module(module_path)
  }

  parse (source, path) {
    this.left   = 0
    this.line   = 1
    this.column = 1
    this.offset = 0
    this.rune = source.codePointAt(0)
    this.path = path || ''
    this.data = source
    this.result.append(this.path)
    this.file = this.result.file
    this.nothis  = false
    this.nosuper = false
    this.iotable = false
    this.begin()
    while (this.rune) {
      let action = match(this.begin(), DECL)
      if (action)
        action = action(this)
      if (!action)
      {
        this.beNote()
        this.nls()
      }
      else
      {
        this.lead()
        this.result.append(action(this))
      }
    }
    return this.result
  }

  get end () {
    return {
      offset: this.offset,
      column: this.column,
      line: this.line
    }
  }

  begin () {
    this.start = this.end
    return this
  }

  lead () {
    this.left = this.start.column
  }

  codePointAt (offset) {
    return this.data.codePointAt(offset) | 0
  }

  text (start, end) {
    return this.data.substring(
      start || this.start.offset, end || this.offset
    )
  }

  beZero (sign) {
    if (sign) this.fail(`unexpected ${keyOf(sign)}`)
  }

  unexpected () {
    let
      str = isVChar(this.rune)
      && String.fromCodePoint(this.rune)
      || this.rune === 0 && 'EOF'
      || 'U+' + toHex(this.rune)
      + (NLS.includes(this.rune) && ' line-break' || ''),
      loc = `${this.path}:${this.line}:${this.column}`

    throw Error(
      `unexpected character ${str} at ${loc}`
    )
  }

  assert (x, ...types) {
    if (
      x && types.length === 0
      || types.some(t => x instanceof t)
    )
      return x


    let names = types.map(t => t.name).join('|')

    this.fail(
      `expected ${toString(x)} to type-of ${names}`,
      x.loc.start
    )
  }

  exists (msg, x) {
    this.fail(`${msg} ${x.normalize()} already exists`, x.loc.start)
  }

  fail (msg, start) {
    start = start || this.start
    msg = typeof msg === 'number'
      && `unexpected ${keyOf(msg)}`
      || msg || 'unexpected token'

    msg = `${msg} at ${this.path}:${start.line}:${start.column}`
    throw Error(msg)
  }

  step () {
    let cp = this.rune
    if (!cp) return 0

    if (cp < 0xD800)
      this.offset++
    else if (cp >= 0xF900 && cp <= 0x10FFFF)
      this.offset += 2
    else
      throw Error(
        `illegal character U+${cp.toString(16)}`
      )

    this.column++
    this.rune = this.codePointAt(this.offset)
    return cp
  }

  drop () {
    this.step()
    return this
  }

  isAlpha () {
    return isAlpha(this.rune)
  }
  isDigit () {
    let cp = this.rune
    return cp >= 0x30 && cp <= 0x39
  }
  isQuoted () {
    return this.rune === 0x22
  }

  isVChar () {
    return isVChar(this.rune)
  }
  isIDS () {
    return isIDS(this.rune)
  }
  isIDC () {
    return isIDC(this.rune)
  }

  soBin () {
    let cp = this.rune
    if (cp === 0x30 || cp === 0x31) {
      this.step()
      return cp - 0x30
    }
    return -256
  }
  soDigit () {
    let cp = this.rune
    if (cp >= 0x30 && cp <= 0x39) {
      this.step()
      return cp - 0x30
    }
    return MIN
  }
  soHex () {
    let cp = this.rune
    if (cp >= 0x30 && cp <= 0x39) {
      this.step()
      return cp - 0x30
    }

    if (cp >= 0x61 && cp <= 0x66) {
      this.step()
      return cp - 0x61 + 10
    }

    if (cp >= 0x41 && cp <= 0x46) {
      this.step()
      return cp - 0x41 + 10
    }
    return MIN
  }

  nl () {
    if (!NLS.includes(this.rune)) return false
    if (this.step() === 0x0D && this.rune === 0x0A)
      this.step()
    this.line++
    this.column = 1
    return true
  }
  nls () {
    while (this.nl());
    return this
  }

  sps () {
    while (this.rune === 0x20) this.step()
    return this
  }

  at () {
    return this.rune === 0x40
      && this.begin().drop() && AT
      || 0
  }

  colon () {
    return this.rune === 0x3A
      && this.begin().drop() && COLON
      || 0
  }

  minus () {
    return this.rune === 0x2D
      && this.begin().drop() && MINUS
      || 0
  }

  comma () {
    return this.rune === 0x2C
      && this.begin().drop() && COMMA
      || 0
  }
  dot () {
    return this.rune === 0x2E
      && this.begin().drop() && DOT
      || 0
  }
  lowline () {
    return this.rune === 0x5F
      && this.begin().drop() && IGNORE
      || 0
  }

  questionMark () {
    return this.rune === 0x3F
      && this.begin().drop() && QMARK
      || 0
  }
  leftParens () {
    return this.rune === 0x28
      && this.begin().step() && LPARENS
      || 0
  }
  rightParens () {
    return this.rune === 0x29
      && this.begin().drop() && RPARENS
      || 0
  }
  leftBraces () {
    return this.rune === 0x7B
      && this.begin().drop() && LBRACES
      || 0
  }
  rightBraces () {
    return this.rune === 0x7D
      && this.begin().drop() && RBRACES
      || 0
  }
  leftBrackets () {
    return this.rune === 0x5B
      && this.begin().drop() && LBRACKETS
      || 0
  }
  rightBrackets () {
    return this.rune === 0x5D
      && this.begin().drop() && RBRACKETS
      || 0
  }

  beId () {
    let sign = this.beRight()
    if (sign === IDENT) return Id(this)
    this.fail(sign)
  }

  asId () {
    let sign = this.beMore()
    if (sign === IDENT || isKeyword(sign))
      return new Identifier(this.text())
        .start(this.start).end(this.end)

    this.fail(sign)
  }

  beAlign () {
    return this.left === this.start.column
       && this
       || this.fail(IllIndent)
  }

  beIndent () {
    return this.left
      && this.left < this.start.column
      && this
      || this.fail(IllIndent)
  }

  isAlign () {
    return this.left === this.column
  }

  isIndent () {
    return this.left && this.left < this.column
  }

  isOutdent () {
    return this.left > this.column || !this.left
  }

  cwsp () {
    while (this.nl() && this.nls().rune === 0x20) {
      while (this.rune === 0x20)
        if (this.drop().step() !== 0x20)
          this.fail(IllIndent)

      if (this.rune === 0x3B)
        this.begin().soNotes()
      else
        break
    }
    if (this.column === 1 || !this.rune)
      this.left = 0
    return this
  }

  right () {
    let
      cp = this.rune,
      sign = match(this.begin(), TOKEN)
    return isKeyword(sign)
      && (
        isIDC(this.rune) && IDC(this)
        || this.rune === 0x2D && IDC(this)
        || sign
      )
      || sign === DOLLAR
      && beDollarIdent(this)
      || !sign && isIDS(cp) && IDC(this)
      || sign
  }

  soRight () {
    return this.sps().right()
      || this.rune === 0x3B
      && this.begin().beNote()
      || 0
  }

  beRight () {
    return this.soRight()
      || this.unexpected()
  }

  soChain () {
    // x.y | x[y] | x()
    if (this.sps().rune === 0x3B)
      this.begin().beNote()

    if (!this.cwsp().left || this.left >= this.column)
      return 0

    return this.dot()
      || this.leftBrackets()
      || this.leftParens()
      || 0
  }

  soMore () {
    if (!this.left || this.left > this.column)
      return 0

    if (this.left < this.column) {
      let sign = this.soRight()
        || this.cwsp().left
        && this.left < this.column
        && (this.right() || this.unexpected())
        || 0

      if (sign || !this.left || this.left > this.column)
        return sign
    }

    return this.rightBrackets()
      || this.rightParens()
      || this.rightBraces()
      || this.leftBraces()
      || 0
  }

  beMore () {
    return this.soMore()
      || this.fail('unexpected eol')
  }

  beNext () {
    let sign = this.soRight()
    if (sign) this.fail(sign)

    if (!this.cwsp().isIndent())
      this.fail(IllIndent)

    return this.beMore()
  }

  beNote () {
    while (isVChar(this.rune)) this.step()
    let text = this.text()
    if (text)
      this.result.append(
        new Note(this.text()).start(this.start).end(this.end)
      )
  }

  soNotes () {
    let
      text = '',
      col = this.column,
      start = this.start

    if (this.drop().rune !== 0x3B)
      return this.beNote()

    while (true) {
      while (isVChar(this.rune)) this.step()

      text += this.text() + '\n'
      if (!this.nl()) this.unexpected()
      while (this.nl()) text += '\n'

      while (this.rune === 0x20) {
        if (this.drop().step() !== 0x20)
          this.fail(IllIndent)
        if (col === this.column) break
      }

      if (col !== this.column) this.unexpected()

      this.begin()
      if (NLS.includes(this.rune)) this.unexpected()
      if (this.rune !== 0x3B || this.drop().rune !== 0x3B) {
        if (NLS.includes(this.rune)) this.unexpected()
        continue
      }
      while (isVChar(this.rune)) this.step()
      text += this.text()
      break
    }
    this.result.append(
      new Note(text).start(start).end(this.end)
    )
  }

  keyOf (sign) {
    return keyOf(sign)
  }

}
